﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MipsMachine
{
    public delegate T ReadRegister<T>();
    public delegate void WriteRegister<T>(T value);

	public class MipsMachine : IEnumerable<MipsInstruction>
	{
		public delegate void BreakEvent();

        public int[] registers;
		protected int pc;
		protected int memory_size;

		protected MipsProgram program;
		protected volatile bool do_break;
		protected volatile bool do_exit;

		public event Action<string> Callback;

		protected MipsInstruction[] memory;

		public MipsProgram Program { get { return program; } }

		public int PC { get { return pc; } }

		public int GetRegister(Register index) { return registers[(int)index]; }
		public void SetRegister(Register index, int value) { registers[(int)index] = value; }

		public int MemoryBytes { get { return memory_size; } }

		public void Stop()
		{
			do_exit = true;
		}


		public MipsInstruction GetInstruction(int index)
		{
			return memory[index];
		}
		public void SetInstruction(int index, MipsInstruction ins)
		{
			memory[index] = ins;
		}

        public MipsMachine()
        {
            registers = new int[32];
        }

        public ReadRegister<int> GetRegisterReadFunc(Register reg)
        {
            if ((int)reg == 0)
                return () =>  0;
            else
                return () => registers[(int)reg];
        }
        public WriteRegister<int> GetRegisterWriteFunc(Register reg)
        {
            if ((int)reg == 0)
                return val => { };
            else
                return val => registers[(int)reg] = val;
        }
		
		private unsafe void SysCall()
		{
			string ret = null;
			switch (registers[(int)Register.v0])
			{
				case 0:	// Write integer
					ret = registers[(int)Register.a0].ToString();
					break;
				case 4:	// Write string
					fixed (MipsInstruction* ptr = memory)
					{
						StringBuilder bldr = new StringBuilder();
						int index = registers[(int)Register.a0];
						byte* p = ((byte*)ptr) + index;
						while (index < memory.Length && *p != 0)
						{
							bldr.Append((char)*p++);
						}
						ret = bldr.ToString();
					}
					
					break;
				case 10:	// Exit
					do_exit = true;
					break;
				case 11:	// Write character
					ret = ((char)registers[(int)Register.a0]).ToString();
					break;
			}
			if (ret != null && Callback != null)
				Callback(ret);
		}

		/// <summary>
		/// Triggers a debug break event which will preferably halt the machine on next cycle
		/// </summary>
		public void DebugBreak()
		{
			do_break = true;
		}

		/// <summary>
		/// The DebugBreak event triggers when either DebugBreak is called or a break condition is met
		/// </summary>
		public event BreakEvent Break;


		public unsafe bool Step()
		{
			if (memory_size <= 0)
				return false;
			WriteRegister<int> rd;
			ReadRegister<int> rs;

			ReadRegister<int> rt;
			WriteRegister<int> wt;
			if (pc < memory.Length && !do_exit)
			{

				if (do_break && Break != null)
					Break();
				var instruction = memory[pc / 4];


				rs = GetRegisterReadFunc(instruction.Rs);
				int tmp = 0;
				switch (instruction.Type)
				{
					case InstructionType.IType:
						wt = GetRegisterWriteFunc(instruction.Rt);
						switch (instruction.OpCode)
						{
							case OpCode.ADDI:
								wt(rs() + (int)instruction.GetSignedImmediate());
								pc += 4;
								break;
							case OpCode.ADDU:
								wt((int)((uint)rs() + instruction.Immediate));
								pc += 4;
								break;
							case OpCode.ANDI:
								wt(rs() & (int)instruction.GetSignedImmediate());
								pc += 4;
								break;

							case OpCode.BEQ:
								rt = GetRegisterReadFunc(instruction.Rt);
								if (rs() == rt())
									pc += instruction.GetSignedImmediate() * 4; // the program counter still adds 4, branch or not
								else
									pc += 4;
								break;
							case OpCode.BLTZ:
								if (rs() < 0)
									pc += instruction.GetSignedImmediate() * 4;
								else
									pc += 4;
								break;
							case OpCode.BNE:
								rt = GetRegisterReadFunc(instruction.Rt);
								if (rs() != rt())
									pc += instruction.GetSignedImmediate() * 4; // the program counter still adds 4, branch or not
								else
									pc += 4;
								break;
							case OpCode.LB:
								tmp = (int)(rs() + instruction.Immediate);
								if (tmp < 0 || tmp >= memory_size)
									throw new IndexOutOfRangeException("Memory read out of bounds");
								fixed (MipsInstruction* ins = memory)
								{
									wt((int)*(((byte*)ins) + tmp));
								}
								pc += 4;
								break;
							case OpCode.LUI:
								wt((int)instruction.Immediate << 16);
								pc += 4;
								break;
							case OpCode.LW:
								tmp = (int)(rs() + instruction.Immediate);
								if (tmp < 0 || tmp >= memory_size)
									throw new IndexOutOfRangeException("Memory read out of bounds");
								fixed (MipsInstruction* ins = memory)
								{
									wt((int)*(short*)(((byte*)ins) + tmp));
								}
								pc += 4;
								break;
							case OpCode.ORI:
								wt((rs() | (int)instruction.GetSignedImmediate()));
								pc += 4;
								break;
							case OpCode.SB:
								rt = GetRegisterReadFunc(instruction.Rt);
								tmp = (int)(rs() + instruction.Immediate);
								if (tmp < 0 || tmp >= memory_size)
									throw new IndexOutOfRangeException("Memory write out of bounds");
								fixed (MipsInstruction* ins = memory)
								{
									byte* ptr = ((byte*)ins) + tmp;
									*ptr = (byte)rt();
								}
								pc += 4;
								break;
							case OpCode.SW:
								rt = GetRegisterReadFunc(instruction.Rt);
								tmp = (int)(rs() + instruction.Immediate);
								if (tmp < 0 || tmp >= memory.Length)
									throw new IndexOutOfRangeException("Memory write out of bounds");
								fixed (MipsInstruction* ins = memory)
								{
									short* ptr = (short*)(((byte*)ins) + tmp);
									*ptr = (short)rt();
								}
								pc += 4;
								break;
							case OpCode.SLTI:
								wt(rs() < instruction.GetSignedImmediate() ? 1 : 0);
								pc += 4;
								break;
							case OpCode.SLTIU:
								wt((uint)rs() < (uint)instruction.Immediate ? 1 : 0);
								pc += 4;
								break;
							case OpCode.XORI:
								wt(rs() ^ (int)instruction.GetSignedImmediate());
								pc += 4;
								break;
							default:
								DebugBreak();
								pc += 4;
								break;
						}
						break;
					case InstructionType.RType:
						rd = GetRegisterWriteFunc(instruction.Rd);
						rt = GetRegisterReadFunc(instruction.Rt);
						switch (instruction.Funct)
						{
							case Function.ADD:
								rd(rt() + rs());
								pc += 4;
								break;
							case Function.AND:
								rd(rt() & rs());
								pc += 4;
								break;
							case Function.DIV:
								rd(rt() / rs());
								pc += 4;
								break;
							case Function.DIVU:
								rd((int)((uint)rt() / (uint)rs()));
								pc += 4;
								break;
							case Function.MULT:
								rd(rt() * rs());
								pc += 4;
								break;
							case Function.MULTU:
								rd((int)((uint)rt() * (uint)rs()));
								pc += 4;
								break;
							case Function.OR:
								rd(rt() & rs());
								pc += 4;
								break;
							case Function.JR:
								pc = rt();
								break;
							case Function.SLL:
								rd(rt() << (int)instruction.Shamt);
								pc += 4;
								break;
							case Function.SLLV:
								rd(rt() << rs());
								pc += 4;
								break;
							case Function.SLT:
								rd(rs() < rt() ? 1 : 0);
								pc += 4;
								break;
							case Function.SLTU:
								rd((uint)rs() < (uint)rt() ? 1 : 0);
								pc += 4;
								break;
							case Function.SRA:
								rd(rs() >> (int)instruction.Shamt);
								pc += 4;
								break;
							case Function.SRL:
								rd(rs() >> (int)instruction.Shamt);
								pc += 4;
								break;
							case Function.SRLV:
								rd(rs() >> rt());
								pc += 4;
								break;
							case Function.SUB:
								rd(rs() - rt());
								pc += 4;
								break;
							case Function.SUBU:
								rd((int)((uint)rs() - (uint)rt()));
								pc += 4;
								break;
							case Function.SYSCALL:
								SysCall();
								pc += 4;
								break;
							case Function.XOR:
								rd(rs() ^ rt());
								pc += 4;
								break;
							default:
								DebugBreak();
								break;
						}
						break;
					case InstructionType.JType:
						switch (instruction.OpCode)
						{
							case OpCode.J:
								pc = (int)instruction.Address * 4;
								break;
							case OpCode.JAL:
								registers[31] = pc + 4;
								pc = (int)instruction.Address * 4;
								break;
							default:
								DebugBreak();
								break;
						}
						break;
				}
				return true;
			}
			else
				return false;
		}

		public void Initialize(int mem_size)
		{
			//MipsInstruction* stack = stackalloc MipsInstruction[memory_size];
			memory = new MipsInstruction[mem_size];


			// Copy program into machine memory
			for (int i = 0; i < program.Program.Count; i++)
				memory[i] = program.Program[i];

			for (int i = 0; i < registers.Length; i++)
				registers[i] = 0;
			// Set stack pointer after program code
			registers[(int)Register.sp] = program.Program.Count * 4;
			pc = program.EntryPoint;

			memory_size = mem_size * 4;
		}

		/// <summary>
		/// Executes the virtual machine allocating a user defined stack size
		/// </summary>
		/// <param name="memory_size">Stack size in 4 byte blocks</param>
		public void Run()
		{
			if (memory_size <= 0)
				throw new System.InvalidOperationException("Machine has not been initialized.");

			while (Step()) 
				System.Windows.Forms.Application.DoEvents();

		}

		public IAsyncResult BeginRun(AsyncCallback callback, object @object)
		{
			Action run = () =>
				{
					if (memory_size <= 0)
						throw new System.InvalidOperationException("Machine has not been initialized.");

					while (Step())
						;
				};
			return run.BeginInvoke(callback, @object);
		}

		public void BeginDebug() {
			Step();
		}

		public MipsMachine(MipsProgram program)
			: this()
		{
			this.program = program;
		}


		#region IEnumerable<MipsInstruction> Members

		public IEnumerator<MipsInstruction> GetEnumerator()
		{
			return memory.GetEnumerator() as IEnumerator<MipsInstruction>;
		}

		#endregion

		#region IEnumerable Members

		System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
		{
			return memory.GetEnumerator();
		}

		#endregion
	}
	/// <summary>
	/// Indicates the current state of a virtual machine
	/// </summary>
	public enum MachineState
	{
		Running,
		Break,
		Ended,
		Aborted
	}
}
